package de.tabreaderandroid.tabreader.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.util.SparseArray;
import de.tabreaderandroid.tabreader.model.BarGroup;
import de.tabreaderandroid.tabreader.model.ChordedLine;
import de.tabreaderandroid.tabreader.model.MetaLine;
import de.tabreaderandroid.tabreader.model.TabFile;
import de.tabreaderandroid.tabreader.model.TabFileElement;
import de.tabreaderandroid.tabreader.model.TabListItem;
import de.tabreaderandroid.tabreader.model.TabString;

/**
 * Utility class for handling tab files.
 * 
 * @author André Stannek
 * 
 */
public final class TabFileUtils {

	private static final String BAR_SEPERATOR = "\\|";
	private static final String NOTES = "[A-Ha-h]";
	private static final String ACCIDENTALS = "[#b]?";
	private static final String INTERVAL = "[1-9|10|11]";
	private static final String QUALITY = "([m|maj|min|M|major|minor|sus|add]"
			+ INTERVAL + "?)*";
	private static final String CHORD = NOTES + ACCIDENTALS + QUALITY;
	private static final String CHORD_LINE = "^\\s*(" + CHORD + "\\s*)+$";
	private static final String TAB_LINE = NOTES + ACCIDENTALS + "\\|-.*";

	/**
	 * Scans a folder for contained tab files by certain criteria.
	 * 
	 * @see TabFileUtils#isTabFile(File)
	 * 
	 * @param folder
	 *            the folder containing the files to be listed
	 * @param recursive
	 *            if {@code true}, subfolders of the given folder will be
	 *            searched
	 * @return the tab files contained in the given folder
	 */
	public static List<TabListItem> getTabFilesFromFolder(final File folder,
			final boolean recursive) {
		List<TabListItem> result = new ArrayList<TabListItem>();
		for (File file : folder.listFiles()) {
			if (file.isDirectory() && recursive) {
				result.addAll(getTabFilesFromFolder(file, true));
			} else if (file.isFile() && isTabFile(file)) {
				result.add(TabListItem.getFromFile(file));
			}
		}
		return result;
	}

	/**
	 * Determines if a {@link File} is (possibly) a valid tab file.<br>
	 * <br>
	 * Criteria right now are:
	 * 
	 * <ul>
	 * <li>mimetype has to be {@code "text/plain"}
	 * </ul>
	 * 
	 * @param file
	 *            the {@link File} to be checked
	 * @return true if the given {@link File} is (possibly) a tab file, false if
	 *         it is definitly not
	 */
	public static boolean isTabFile(final File file) {
		String mimeType = URLConnection.guessContentTypeFromName(file
				.getAbsolutePath());
		return (mimeType != null && mimeType.equals("text/plain"));
	}

	/**
	 * Masking utility class constructor.
	 */
	private TabFileUtils() {
		super();
	}

	/**
	 * Reads the text from a tab file.
	 * 
	 * @param filePath
	 *            the path to the tab file
	 * @return the text contained in the given tab file
	 * @throws InvalidTabFormatException
	 *             if parsing of the tab was not possible
	 */
	public static TabFile parseTabFile(final String filePath)
			throws InvalidTabFormatException {
		List<ChordedLine> chordLines = new ArrayList<ChordedLine>();
		List<BarGroup> tabLines = new ArrayList<BarGroup>();
		List<MetaLine> metaLines = new ArrayList<MetaLine>();
		BufferedReader reader = null;
		StringBuilder originText = new StringBuilder();
		int index = 0;
		try {
			reader = new BufferedReader(new InputStreamReader(
					new FileInputStream(filePath), "UTF-8"));
			String line = null;
			while ((line = reader.readLine()) != null) {
				originText.append(line).append("\n");
				if (line.matches(TAB_LINE)) {
					tabLines.add(parseBarGroup(line, reader, index));
					index += 6;
				} else if (line.matches(CHORD_LINE)) {
					String nextLine = reader.readLine();
					if (nextLine != null && nextLine.matches(TAB_LINE)) {
						tabLines.add(parseBarGroup(nextLine, reader, index));
						index += 7;
					} else {
						chordLines.add(parseChordedLine(line, nextLine, index));
						index += 2;
					}
				} else {
					metaLines.add(new MetaLine(index, line));
					index++;
				}
			}
		} catch (FileNotFoundException e) {
			Log.e(TabFileUtils.class.getName(),
					"Intended tab file was not found: " + filePath, e);
		} catch (IOException e) {
			Log.e(TabFileUtils.class.getName(),
					"Intended tab file could not be read: " + filePath, e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					Log.e(TabFileUtils.class.getName(),
							"FileInputStream could not be closed", e);
				}
			}
		}
		TabFile result = new TabFile(originText.toString());
		result.setTabBars(tabLines);
		result.setChordedLines(chordLines);
		result.setMetaLines(metaLines);
		return result;
	}

	private static ChordedLine parseChordedLine(final String firstLine,
			final String nextLine, final int index) {
		ChordedLine result = new ChordedLine(index);
		result.setChordLine(firstLine);
		result.setLyricsLine(nextLine);
		return result;
	}

	private static BarGroup parseBarGroup(final String firstLine,
			final BufferedReader reader, final int index) throws IOException,
			InvalidTabFormatException {

		BarGroup result = new BarGroup(index);

		String[] strings = new String[6];
		strings[0] = firstLine;
		for (int i = 1; i < 6; i++) {
			strings[i] = reader.readLine();
			if (strings[i] == null || !strings[i].matches(TAB_LINE)) {
				throw new InvalidTabFormatException("Parsing error at line: "
						+ strings[i]);
			}
		}

		for (int i = 0; i < strings.length; i++) {
			result.setString(i, new TabString(strings[i].split(BAR_SEPERATOR)));
		}

		return result;
	}

	/**
	 * Sets the highlighting for specific elements in tabs.
	 * 
	 * @param tab
	 *            the tab file to highlight
	 * @param maxCharWidth
	 * @throws InvalidTabFormatException
	 *             if processing of the tab content is erroneous
	 */
	public static Spannable getSpans(final TabFile tab, final int maxCharWidth)
			throws InvalidTabFormatException {

		List<TabFileElement> tabElements = tab.getTabElements();
		SparseArray<TabFileElement> map = new SparseArray<TabFileElement>();
		List<String> allLines = new ArrayList<String>();
		int lineCount = 0;
		for (TabFileElement element : tabElements) {
			map.put(lineCount, element);
			List<String> lines = element.getLines(maxCharWidth);
			lineCount += lines.size();
			allLines.addAll(lines);
		}

		StringBuilder builder = new StringBuilder();
		for (String line : allLines) {
			builder.append(line).append("\n");
		}
		String text = builder.toString();
		Spannable result = new SpannableString(text);

		int offset = 0;
		Pattern chordPattern = Pattern.compile(CHORD);
		Class<? extends TabFileElement> lastType = null;
		int lastTypedLine = -1;
		for (int i = 0; i < allLines.size(); i++) {
			TabFileElement element = map.get(i);
			if (element != null) {
				lastTypedLine = i;
				lastType = element.getClass();
			}
			applyStyle(allLines, result, offset, chordPattern, i,
					lastTypedLine, lastType);
			offset = offset + allLines.get(i).length() + 1;
		}
		return result;
	}

	private static void applyStyle(final List<String> allLines,
			final Spannable result, final int offset,
			final Pattern chordPattern, final int lineToStyle,
			final int lastTypedLine, final Class<? extends TabFileElement> type) {
		if (type.equals(ChordedLine.class)) {

			if ((lineToStyle - lastTypedLine) % 2 == 0) {
				Matcher matcher = chordPattern.matcher(allLines
						.get(lineToStyle));
				while (matcher.find()) {
					result.setSpan(new ForegroundColorSpan(Color.BLUE),
							matcher.start() + offset, matcher.end() + offset,
							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
				}
			}
		} else if (type.equals(MetaLine.class)) {
			result.setSpan(new ForegroundColorSpan(Color.RED), offset, allLines
					.get(lineToStyle).length() + offset,
					Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
		}
	}
}
